package org.mspring.platform.support.sitemap;

import static java.util.Calendar.HOUR_OF_DAY;
import static java.util.Calendar.MILLISECOND;
import static java.util.Calendar.MINUTE;
import static java.util.Calendar.SECOND;

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * <p>
 * Formats and parses dates in the six defined W3C date time formats. These formats are described in "Date and Time Formats", <a href="http://www.w3.org/TR/NOTE-datetime" >http://www.w3.org/TR/NOTE-datetime</a>.
 * </p>
 *
 * <p>
 * The formats are:
 *
 * <ol>
 * <li>YEAR: YYYY (eg 1997)
 * <li>MONTH: YYYY-MM (eg 1997-07)
 * <li>DAY: YYYY-MM-DD (eg 1997-07-16)
 * <li>MINUTE: YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
 * <li>SECOND: YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
 * <li>MILLISECOND: YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
 * </ol>
 *
 * Note that W3C timezone designators (TZD) are either the letter "Z" (for GMT) or a pattern like "+00:30" or "-08:00". This is unlike RFC 822 timezones generated by SimpleDateFormat, which omit the ":" like this: "+0030" or "-0800".
 * </p>
 *
 * <p>
 * This class allows you to either specify which format pattern to use, or (by default) to automatically guess which pattern to use (AUTO mode). When parsing in AUTO mode, we'll try parsing using each pattern until we find one that works. When formatting in AUTO mode, we'll use this algorithm:
 *
 * <ol>
 * <li>If the date has fractional milliseconds (e.g. 2009-06-06T19:49:04.45Z) we'll use the MILLISECOND pattern
 * <li>Otherwise, if the date has non-zero seconds (e.g. 2009-06-06T19:49:04Z) we'll use the SECOND pattern
 * <li>Otherwise, if the date is not at exactly midnight (e.g. 2009-06-06T19:49Z) we'll use the MINUTE pattern
 * <li>Otherwise, we'll use the DAY pattern. If you want to format using the MONTH or YEAR pattern, you must declare it explicitly.
 * </ol>
 *
 * Finally note that, like all classes that inherit from DateFormat, <b>this class is not thread-safe</b>. Also note that you can explicitly specify the timezone to use for formatting using the {@link #setTimeZone(TimeZone)} method.
 *
 * @author Dan Fabulich
 * @see <a href="http://www.w3.org/TR/NOTE-datetime">Date and Time Formats</a>
 */
public class W3CDateFormat extends SimpleDateFormat {

    private static final long serialVersionUID = -5733368073260485802L;

    /**
     * The six patterns defined by W3C, plus {@link #AUTO} configuration
     */
    public enum Pattern {

        /**
         * "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
         */
        MILLISECOND("yyyy-MM-dd'T'HH:mm:ss.SSSZ", true),
        /**
         * "yyyy-MM-dd'T'HH:mm:ssZ"
         */
        SECOND("yyyy-MM-dd'T'HH:mm:ssZ", true),
        /**
         * "yyyy-MM-dd'T'HH:mmZ"
         */
        MINUTE("yyyy-MM-dd'T'HH:mmZ", true),
        /**
         * "yyyy-MM-dd"
         */
        DAY("yyyy-MM-dd", false),
        /**
         * "yyyy-MM"
         */
        MONTH("yyyy-MM", false),
        /**
         * "yyyy"
         */
        YEAR("yyyy", false),
        /**
         * Automatically compute the right pattern to use
         */
        AUTO("", true);

        private final String pattern;
        private final boolean includeTimeZone;

        Pattern(String pattern, boolean includeTimeZone) {
            this.pattern = pattern;
            this.includeTimeZone = includeTimeZone;
        }
    }

    private final Pattern pattern;
    /**
     * The GMT ("zulu") time zone, for your convenience
     */
    public static final TimeZone ZULU = TimeZone.getTimeZone("GMT");

    /**
     * Build a formatter in AUTO mode
     */
    public W3CDateFormat() {
        this(Pattern.AUTO);
    }

    /**
     * Build a formatter using the specified Pattern, or AUTO mode
     */
    public W3CDateFormat(Pattern pattern) {
        super(pattern.pattern);
        this.pattern = pattern;
    }

    /**
     * This is what you override when you extend DateFormat; use {@link DateFormat#format(Date)} instead
     *
     * @return
     */
    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        boolean includeTimeZone = pattern.includeTimeZone;
        if (pattern == Pattern.AUTO) {
            includeTimeZone = autoFormat(date);
        }
        super.format(date, toAppendTo, pos);
        if (includeTimeZone) {
            convertRfc822TimeZoneToW3c(toAppendTo);
        }
        return toAppendTo;
    }

    private boolean applyPattern(Pattern pattern) {
        applyPattern(pattern.pattern);
        return pattern.includeTimeZone;
    }

    private boolean autoFormat(Date date) {
        if (calendar == null) {
            calendar = new GregorianCalendar();
        }
        calendar.setTime(date);
        boolean hasMillis = calendar.get(MILLISECOND) > 0;
        if (hasMillis) {
            return applyPattern(Pattern.MILLISECOND);
        }
        boolean hasSeconds = calendar.get(SECOND) > 0;
        if (hasSeconds) {
            return applyPattern(Pattern.SECOND);
        }
        boolean hasTime = (calendar.get(HOUR_OF_DAY) + calendar.get(MINUTE)) > 0;
        if (hasTime) {
            return applyPattern(Pattern.MINUTE);
        }
        return applyPattern(Pattern.DAY);
    }

    /**
     * This is what you override when you extend DateFormat; use {@link DateFormat#parse(String)} instead
     *
     * @return
     */
    @Override
    public Date parse(String text, ParsePosition pos) {
        text = convertW3cTimeZoneToRfc822(text);
        if (pattern == Pattern.AUTO) {
            return autoParse(text, pos);
        }
        return super.parse(text, pos);
    }

    private Date autoParse(String text, ParsePosition pos) {
        for (Pattern p : Pattern.values()) {
            if (p == Pattern.AUTO) {
                continue;
            }
            applyPattern(p);
            Date out = super.parse(text, pos);
            if (out != null) {
                return out;
            }
        }
        return null; // this will force a ParseException
    }

    private void convertRfc822TimeZoneToW3c(StringBuffer toAppendTo) {
        int length = toAppendTo.length();
        if (ZULU.equals(calendar.getTimeZone())) {
            toAppendTo.replace(length - 5, length, "Z");
        } else {
            toAppendTo.insert(length - 2, ':');
        }
    }

    private String convertW3cTimeZoneToRfc822(String source) {
        int length = source.length();
        if (source.endsWith("Z")) {
            return source.substring(0, length - 1) + "+0000";
        }
        if (source.charAt(length - 3) == ':') {
            return source.substring(0, length - 3) + source.substring(length - 2);
        }
        return source;
    }

}
